//
//  YCDatabase.m
//  YCDB
//
//  Created by 唐可雄 on 2017/8/11.
//  Copyright © 2017年 swustyc. All rights reserved.
//

#import "YCDatabase.h"

#import <sqlite3.h>

#define YCDefaultBusyRetryTimeout 2


@interface YCDatabase () {
    /** 数据库指针 */
    void*               _db;
}

/** 数据库地址 */
@property (nonatomic, assign) NSURL *url;
/** 数据库读写锁轮询超时时间 */
@property (nonatomic, assign) NSTimeInterval busyRetryTimeout;
/** 数据库读写锁第一次询问的时间点 */
@property (nonatomic, assign) NSTimeInterval busyRetryStart;

@end

@implementation YCDatabase

#pragma mark - Life Cycle

+ (instancetype)databaseWithURL:(NSURL *)url {
    return [[YCDatabase alloc] initAtURL:url];
}

/**
 数据库初始化

 @param url 数据库地址，nil、""和"xxx"三种情况
 @return 数据库对象
 */
- (instancetype)initAtURL:(NSURL *)url {
    assert(sqlite3_threadsafe());
    
    self = [super init];
    
    if (self) {
        _url = url;
        _busyRetryTimeout = YCDefaultBusyRetryTimeout;
    }
    
    return self;
}

+ (instancetype)databaseAtURL:(NSURL *)url {
    return [[self alloc] initAtURL:url];
}

#pragma mark - SQLite Info

/**
 SQLite框架的版本

 @return 版本号
 */
+ (NSString *)sqliteLibVersion {
    return [NSString stringWithFormat:@"%s", sqlite3_libversion()];
}

/**
 SQLite是否线程安全

 @return YES/NO，线程安全/不安全
 */
+ (BOOL)isSQLiteThreadSafe {
    return sqlite3_threadsafe() != 0;
}

- (const char *)sqlitePath {
    NSString *path = _url.path;
    
    if (path == nil) {
        // 表示要建立一个In-Memory Database
        return ":memory:";
    }
    if ([path isEqualToString:@""]) {
        // 表示要建立一个Temporary Database
        return "";
    }
    
    return [path fileSystemRepresentation];
}

#pragma mark - Database Config

/**
 当检测到当前连接的数据库处于SQLITE_BUSY状态下，在超时时间内回调函数
 
 @param ptr 数据库对象
 @param count 回调次数
 @return 1/0，不断尝试操作数据库/不再尝试再次访问数据库
 */
int YCDatabaseBusyHandler(void *ptr, int count) {
    YCDatabase *database = (__bridge YCDatabase *)(ptr);
    
    if (count == 0) {
        // 第一次调用
        database.busyRetryStart= [NSDate timeIntervalSinceReferenceDate];
        return 1;
    } else {
        NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - database.busyRetryStart;
        if (delta < database.busyRetryTimeout) {
            // 还未超时
            // 休眠一定时间，[50,100)毫秒
            int sleepTimeRequested = (int)arc4random_uniform(50) + 50;
            int sleepTimeActual = sqlite3_sleep(sleepTimeRequested);
            if (sleepTimeActual != sleepTimeRequested) {
                // 目标休眠时间与实际休眠时间不匹配，可能是因为数据库没有HAVE_USLEEP=1
            }
            // 继续回调
            return 1;
        }
        // 已经超时了
        return 0;
    }
}

/**
 设置数据库读写锁轮询超时时间，每次轮询间隔为[50,100)毫秒之间的一个数

 @param timeout 时间间隔
 */
- (void)setBusyRetryTimeout:(NSTimeInterval)timeout {
    _busyRetryTimeout = timeout;
    
    if (!_db) {
        return;
    }
    
    if (timeout > 0) {
        // 一直等待，直到获得锁
        sqlite3_busy_handler(_db, &YCDatabaseBusyHandler, (__bridge void *)(self));
    } else {
        sqlite3_busy_handler(_db, nil, nil);
    }
}

#pragma mark - Database Open or Close

- (BOOL)open {
    if (_db) {
        // 已经开启了数据库
        return YES;
    }
    
    int error = sqlite3_open([self sqlitePath], (sqlite3**)&_db);
    if (error != SQLITE_OK) {
        // 数据库不能开启
        NSLog(@"数据库开启失败 %d", error);
        return NO;
    } else {
        // 能开启数据库
        // 设置数据库读写锁轮询超时时间，获取数据库读写锁
        self.busyRetryTimeout = _busyRetryTimeout;
        return YES;
    }
}

/**
 开启数据库

 条件：SQLite >= 3.5

 @param flags 数据库打开模式，参考SQLITE_OPEN_READONLY、SQLITE_OPEN_READWRITE
 @param vfsName VFS模块使用的名称
 @return 开启数据库是否成功，YES/NO，成功/失败
 */
- (BOOL)openWithFlags:(int)flags vfs:(nullable NSString *)vfsName {
#if SQLITE_VERSION_NUMBER >= 3005000
    if (_db) {
        // 已经开启了数据库
        return YES;
    }
    int error = sqlite3_open_v2([self sqlitePath], (sqlite3**)&_db, flags, [vfsName UTF8String]);
    if (error != SQLITE_OK) {
        // 数据库不能开启
        NSLog(@"数据库开启失败");
        return NO;
    } else {
        // 能开启数据库
        // 设置数据库读写锁轮询超时时间，获取数据库读写锁
        self.busyRetryTimeout = _busyRetryTimeout;
        return YES;
    }
#else
    // SQLite版本低于了3.5
    NSLog(@"openWithFlags:vfs 要求数据库版本高于3.5");
    return NO;
#endif
}

- (BOOL)close {
    if (!_db) {
        // 已经关闭了数据库
        return YES;
    }
    
    BOOL retry;
    BOOL tryFinalizingOpenStatement = NO;
    do {
        retry = NO;
        int error = sqlite3_close(_db);
        if (error == SQLITE_BUSY || error == SQLITE_LOCKED) {
            // 数据库忙或者被锁
            // 释放资源
            if (!tryFinalizingOpenStatement) {
                tryFinalizingOpenStatement = YES;
                sqlite3_stmt *pStmt;
                while ((pStmt = sqlite3_next_stmt(_db, nil)) != 0) {
                    // 关闭遗漏的资源
                    sqlite3_finalize(pStmt);
                    retry = YES;
                }
            }
        } else if (error != SQLITE_OK) {
            // 关闭失败
            NSLog(@"关闭数据库失败 %d", error);
        }
    } while (retry);
    
    
    _db = nil;
    return YES;
}

#pragma mark - SQLite Bind

/**
 绑定指定对象到准备语句的第几列

 @param obj 对象
 @param cidx 第几列，从1开始
 @param pStmt 准备语句
 */
- (void)bindObject:(id)obj toColumnIndex:(int)cidx inStatement:(sqlite3_stmt *)pStmt {
    if (!obj || obj == [NSNull null]) {
        sqlite3_bind_null(pStmt, cidx);
    } else if ([obj isKindOfClass:[NSData class]]) {
        const void *bytes = [obj bytes];
        // 如果这是一个空的NSData对象，比如[NSData data]，那么bytes就会为nil，SQLite将会绑定为null，而不是blob
        if (!bytes) {
            bytes = "";
        }
        sqlite3_bind_blob(pStmt, cidx, bytes, (int)[obj length], SQLITE_STATIC);
    } else if ([obj isKindOfClass:[NSNumber class]]) {
        if (strcmp([obj objCType], @encode(char)) == 0) {
            sqlite3_bind_int(pStmt, cidx, [obj charValue]);
        } else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
            sqlite3_bind_int(pStmt, cidx, [obj unsignedCharValue]);
        } else if (strcmp([obj objCType], @encode(short)) == 0) {
            sqlite3_bind_int(pStmt, cidx, [obj shortValue]);
        } else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
            sqlite3_bind_int(pStmt, cidx, [obj unsignedShortValue]);
        } else if (strcmp([obj objCType], @encode(int)) == 0) {
            sqlite3_bind_int(pStmt, cidx, [obj intValue]);
        } else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
            sqlite3_bind_int64(pStmt, cidx, [obj unsignedIntValue]);
        } else if (strcmp([obj objCType], @encode(long)) == 0) {
            sqlite3_bind_int64(pStmt, cidx, [obj longValue]);
        } else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
            sqlite3_bind_int64(pStmt, cidx, [obj unsignedLongValue]);
        } else if (strcmp([obj objCType], @encode(long long)) == 0) {
            sqlite3_bind_int64(pStmt, cidx, [obj longLongValue]);
        } else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
            sqlite3_bind_int64(pStmt, cidx, [obj unsignedLongLongValue]);
        } else if (strcmp([obj objCType], @encode(float)) == 0) {
            sqlite3_bind_double(pStmt, cidx, [obj floatValue]);
        } else if (strcmp([obj objCType], @encode(double)) == 0) {
            sqlite3_bind_double(pStmt, cidx, [obj doubleValue]);
        } else if (strcmp([obj objCType], @encode(BOOL)) == 0) {
            sqlite3_bind_int(pStmt, cidx, [obj boolValue] ? 1 : 0);
        } else {
            sqlite3_bind_text(pStmt, cidx, [[obj description] UTF8String], -1, SQLITE_STATIC);
        }
    } else {
        sqlite3_bind_text(pStmt, cidx, [[obj description] UTF8String], -1, SQLITE_STATIC);
    }
}

#pragma mark - SQLite Execute

/**
 执行查询

 @param sql 查询语句，参数可变，通过?来进行占位
 @return 查询结果集
 */
- (YCResultSet *)executeQuery:(NSString *)sql, ... {
    va_list args;
    va_start(args, sql);
    
    sqlite3_stmt *pStmt = 0x00;
    int error = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
    if (error != SQLITE_OK) {
        // 构建准备语句出现错误
        NSLog(@"构建准备语句出现错误");
        // 释放准备的语句
        sqlite3_finalize(pStmt);
        return nil;
    } else {
        // 成功构建准备语句
        // 替换占位符
        int index = 0;
        int count = sqlite3_bind_parameter_count(pStmt);
        while (index < count) {
            id obj = va_arg(args, id);
            index++;
            [self bindObject:obj toColumnIndex:index inStatement:pStmt];
        }
        // 占位符数量和参数数量比较
        if (index != count) {
            // 占位符数量和参数数量不一致
            NSLog(@"占位符数量和参数数量不一致");
            sqlite3_finalize(pStmt);
            return nil;
        } else {
            
        }
    }
    
    va_end(args);
    
    return result;
}

@end
